use num_bigint::BigUint;
use tokio::{
    sync::{mpsc, watch},
    time::{self, Duration},
};
use tracing::info;

use bc_base::{
    self, BlockHeight, BlockId, PrivateKeyBytes, PublicKeyBytes, PublicKeyHashBytes,
    block::{self, Block},
    constants::{ALICE_PRIVATE_KEY, ALICE_PUBLICK_KEY, GENESIS_ACCOUNT, PRIVATE_KEY_BYTES_LENGTH},
    transaction::Transaction,
};
use bc_lib::{mining::MiningCommand, transaction};
use bc_manager::{BcManager, BcManagerHandler};
use mini_block_chain::{Args, blockchain_logic};

pub async fn prepare_test_env(
    db_root_dir: std::path::PathBuf,
    args: &Args,
    if_mining: bool,
    enable_log: bool,
) -> (
    BcManagerHandler,
    watch::Sender<bool>,
    watch::Receiver<bool>,
    Option<std::thread::JoinHandle<()>>,
) {
    if enable_log {
        bc_util::init_logger_for_test(None); // MARK
    }

    info!("Database root dir: {}", db_root_dir.to_str().unwrap());

    let (mining_manager, mining_cmd_receiver, transaction_receiver) =
        bc_lib::mining::MiningManager::new();

    // Create a watch channel for shutdown signal.
    // Initially, false means "keep running". When set to true, tasks should shut down.
    let (shutdown_tx, shutdown_rx) = watch::channel(false);

    let bc_manager = BcManager::new(
        db_root_dir,
        mining_manager,
        args.get_secret_key_seed(),
        args.get_p2p_listen_address(),
        shutdown_rx.clone(),
    )
    .await;

    let mut task_mine = None;
    if if_mining {
        info!("Start mining task");
        // start mining task
        let t = start_mining_task(
            args,
            bc_manager.clone(),
            mining_cmd_receiver,
            transaction_receiver,
            shutdown_rx.clone(),
        )
        .await;
        task_mine = Some(t);
        time::sleep(Duration::from_millis(100)).await; // wait above tasks to start
    } else {
        info!("Mining task is disabled");
    }

    // run blockchain core logic
    // blockchain_logic::run_blockchain_logic(bc_manager.clone()).await;
    blockchain_logic::check_genesis_info(bc_manager.clone()).await;

    (bc_manager, shutdown_tx, shutdown_rx, task_mine)
}

async fn start_mining_task(
    args: &Args,
    bc_manager: BcManagerHandler,
    mining_cmd_receiver: mpsc::Receiver<MiningCommand>,
    transaction_receiver: mpsc::Receiver<Transaction>,
    shutdown_signal: watch::Receiver<bool>,
) -> std::thread::JoinHandle<()> {
    mini_block_chain::start_mining_task(
        args,
        bc_manager,
        mining_cmd_receiver,
        transaction_receiver,
        shutdown_signal,
    )
}

pub async fn prepare_wallet_for_genesis(bc_manager: BcManagerHandler) {
    let public_key = PublicKeyBytes::from_hex_str(GENESIS_ACCOUNT).unwrap();
    let private_key = PrivateKeyBytes::new([0; PRIVATE_KEY_BYTES_LENGTH]);

    let mut bc_manager = bc_manager.lock().await;
    bc_manager
        .add_keypair(&public_key, &private_key)
        .await
        .unwrap();
    bc_manager.check_genesis_info_in_wallets().await;
}

pub async fn add_alice_keypair_to_wallet(bc_manager: BcManagerHandler) {
    let alice_pub_key = PublicKeyBytes::from_hex_str(ALICE_PUBLICK_KEY).unwrap();
    let alice_priv_key = PrivateKeyBytes::from_hex_str(ALICE_PRIVATE_KEY).unwrap();
    let bc_manager = bc_manager.lock().await;
    bc_manager
        .add_keypair(&alice_pub_key, &alice_priv_key)
        .await
        .unwrap();
}

async fn create_coinbase_and_add_to_block_and_process_block(
    bc_manager: BcManagerHandler,
    work_required: u32,
    block: &mut Block,
    block_height: BlockHeight,
    miner_pubkey_hash: &PublicKeyHashBytes,
) -> (BlockId, Block) {
    let nbits: u32 = work_required;
    let extra_nonce = BigUint::from(block_height as u64);
    let bc_manager = bc_manager.lock().await;
    let coinbase = transaction::create_coinbase_for_mining(
        nbits,
        block_height,
        &extra_nonce,
        miner_pubkey_hash,
        bc_manager.get_subsidy_for_mining().await,
    );

    block.add_transaction(coinbase);

    let merkle_root_hash = block.build_merkle_tree().unwrap();
    block.set_merkle_root(merkle_root_hash);
    let block_id = block.find_and_set_valid_nonce();

    bc_manager.process_block(block).await.unwrap();

    (block_id, block.clone())
}

pub async fn create_block_only_contain_coinbase_and_add_to_blockchain(
    bc_manager: BcManagerHandler,
    prev_block_hash: &BlockId,
    block_height: BlockHeight,
    miner_pubkey_hash: &PublicKeyHashBytes,
) -> (BlockId, Block) {
    let sys_time_now = bc_util::get_current_sys_time_seconds();
    let timestamp = sys_time_now - 1; // for minging test. to avoid BlockConsensusError::TimestampTooOld
    create_block_only_contain_coinbase_and_add_to_blockchain_with_timestamp(
        bc_manager,
        prev_block_hash,
        block_height,
        miner_pubkey_hash,
        timestamp as u32,
    )
    .await
}

pub async fn create_block_only_contain_coinbase_and_add_to_blockchain_with_timestamp(
    bc_manager: BcManagerHandler,
    prev_block_hash: &BlockId,
    block_height: BlockHeight,
    miner_pubkey_hash: &PublicKeyHashBytes,
    block_timestamp: u32,
) -> (BlockId, Block) {
    let work_required = bc_manager
        .lock()
        .await
        .get_next_work_required_for_mining()
        .await;
    let header = block::create_block_header_for_mining(prev_block_hash, work_required);
    let mut block = Block::new(header);

    block.set_timestamp(block_timestamp);

    create_coinbase_and_add_to_block_and_process_block(
        bc_manager,
        work_required,
        &mut block,
        block_height,
        miner_pubkey_hash,
    )
    .await
}
